<?php
/**
 * Model Helper Class - ActiveRecord Pattern Tool
 *
 * 이 클래스는 Model의 Active Record 패턴 기능을 제공합니다.
 *
 * @package model
 * @subpackage    activerecord
 *
 * @author jylee3@simplexi.com
 * @version 1.0
 */
class coreModelActiverecord
{
    /**
     * Active Record Pattern Driver
     * @var coreModelActiverecordDriverCommon
     */
    protected $acDriver;

    /**
     * Database PDO Driver
     * @var coreModelDbDriverCommon
     */
    protected $dbDriver;
    protected $dbDriver_Slave = false;

    /**
     * Is Query Mode
     * @var Boolean
     */
    protected $bQueryMode;

    /**
     * Encrypt Column
     * @var Array
     */
    protected $aEncryptColumn       = array();

    protected $aSelect    = array();
    protected $bDistinct    = false;
    protected $aFrom    = array();
    protected $aJoin    = array();
    protected $aWhere    = array();
    protected $aWherein    = array();
    protected $aLike    = array();
    protected $aGroupby    = array();
    protected $aHaving    = array();
    protected $iLimit    = false;
    protected $iOffset    = false;
    protected $sOrder    = false;
    protected $aOrderby    = array();
    protected $aSet    = array();
    protected $aAliasedTables    = array();
    protected $aStoreArray    = array();

    protected $bDbDebug            = false;

    protected $bProtectIdentifiers    = true;

    protected $sEncryptKey;
    protected $sEncryptIV;

    public function __construct(coreModelActiverecordDriverCommon $acDriver, $mDbDriver, $bQueryMode=false)
    {
        $this->acDriver = $acDriver;

        if (is_array($mDbDriver)) {
            $this->dbDriver = $mDbDriver[0];
            $this->dbDriver_Slave = $mDbDriver[1];
        } else {
            $this->dbDriver = $mDbDriver;
        }

        $this->bQueryMode = $bQueryMode;
    }

    /**
     * Active Record Query Execute
     *
     * <code>
     * $mResult = $this->query('SELECT * FROM TEST');
     * </code>
     *
     * @param    String    $sSql
     *
     * @return    Mixed
     */
    public function query($sSql)
    {
        if ($this->bQueryMode!==false) {
            $this->reset_select();
            $this->reset_write();
            $mResult = $sSql;
        } else {
            if ($this->isSelect($sSql)) {
                $mResult = ($this->dbDriver_Slave instanceof coreModelDbDriverCommon)?
                    $this->dbDriver_Slave->query($sSql, FW_MODEL_RESULT_ROWS, true) :
                    $this->dbDriver->query($sSql, FW_MODEL_RESULT_ROWS, true);

                $this->reset_select();
            } else {
                $mResult = $this->dbDriver->exec($sSql, null);
                $this->reset_write();
            }
        }

        return $mResult;
    }

    /**
     * Setting.. Encrypt Key
     *
     * <code>
     * $this->setEncryptKey($sEncryptKey);
     * </code>
     *
     * @param    String    $sEncryptKey
     *
     * @return     coreModelActiverecord
     */
    public function setEncryptKey($sEncryptKey)
    {
        $this->sEncryptKey = $sEncryptKey;

        return $this;
    }

    /**
     * Setting.. Encrypt IV
     *
     * <code>
     * $this->setEncryptIV($sEncryptIV);
     * </code>
     *
     * @param    String    $sEncryptIV
     *
     * @return    coreModelActiverecord
     */
    public function setEncryptIV($sEncryptIV)
    {
        $this->sEncryptIV = $sEncryptIV;

        return $this;
    }

    /**
     * Encrypt Column
     *
     * @param    Mixed    $mColumn
     *
     * @return    coreModelActiverecord
     */
    public function encrypt($mColumn)
    {
        if (is_array($mColumn)) {
            $this->aEncryptColumn = array_merge($this->aEncryptColumn, $mColumn);
        } else {
            $this->aEncryptColumn[] = $mColumn;
        }

        return $this;
    }

    /**
     * Is... Select Query
     *
     * <code>
     * $bIsSelect = $this->isSelect('SELECT * FROM TABLE');
     * </code>
     *
     * @param    String    $sSql
     *
     * @return    Boolean
     */
    public function isSelect($sSql)
    {
        return (stripos($sSql, 'select')===0 || stripos($sSql, 'call')===0)? true : false;
    }

    /**
     * Setting... Query Mode
     *
     * <code>
     * $this->setQueryMode(true);
     *
     * or
     *
     * $bQueryMode = $this->setQueryMode();
     * </code>
     *
     * @param    Boolean    $bQueryMode
     *
     * @return    Boolean
     */
    public function setQueryMode($bQueryMode=null)
    {
        if (is_bool($bQueryMode)) {
            $this->bQueryMode = $bQueryMode;
        }

        return $this->bQueryMode;
    }

    /**
     * Select
     *
     * Generates the SELECT portion of the query
     *
     * @param    String    $sSelect    조회 대상 항목
     * @param    Mixed    $mEscape    Escape 여부
     *
     * @return    coreModelActiverecord
     */
    public function select($sSelect='*', $mEscape=null)
    {
        if (is_bool($mEscape)) {
            $this->bProtectIdentifiers = $mEscape;
        }

        if (is_string($sSelect)) {
            $sSelect = explode(',', $sSelect);
        }

        foreach ($sSelect as $sVal) {
            $sVal = trim($sVal);

            if ($sVal != '') {
                $this->aSelect[] = (in_array($sVal, $this->aEncryptColumn))? $this->getDecryptSql($sVal, $sVal) : $sVal;
            }
        }

        return $this;
    }

    /**
     * Select Max
     *
     * Generates a SELECT MAX(field) portion of a query
     *
     * @param    String    $sSelect    the field
     * @param    String    $sAlias    an alias
     *
     * @return    coreModelActiverecord
     */
    public function select_max($sSelect = '', $sAlias = '')
    {
        return $this->max_min_avg_sum($sSelect, $sAlias, 'MAX');
    }

    /**
     * Select Min
     *
     * Generates a SELECT MIN(field) portion of a query
     *
     * @param    String    $sSelect    the field
     * @param    String    $sAlias    an alias
     *
     * @return    coreModelActiverecord
     */
    public function select_min($sSelect = '', $sAlias = '')
    {
        return $this->max_min_avg_sum($sSelect, $sAlias, 'MIN');
    }

    /**
     * Select Average
     *
     * Generates a SELECT AVG(field) portion of a query
     *
     * @param    String    $sSelect    the field
     * @param    String    $sAlias    an alias
     *
     * @return    coreModelActiverecord
     */
    public function select_avg($sSelect = '', $sAlias = '')
    {
        return $this->max_min_avg_sum($sSelect, $sAlias, 'AVG');
    }

    /**
     * Select Sum
     *
     * Generates a SELECT SUM(field) portion of a query
     *
     * @param    String    $sSelect    the field
     * @param    String    $sAlias    an alias
     *
     * @return    coreModelActiverecord
     */
    public function select_sum($sSelect = '', $sAlias = '')
    {
        return $this->max_min_avg_sum($sSelect, $sAlias, 'SUM');
    }

    /**
     * Processing Function for the four functions above:
     *
     *    select_max()
     *    select_min()
     *    select_avg()
     *  select_sum()
     *
     * @param    String    $sSelect    the field
     * @param    String    $sAlias    an alias
     * @param    String    $sType    type
     *
     * @return    coreModelActiverecord
     */
    public function max_min_avg_sum($sSelect = '', $sAlias = '', $sType = 'MAX')
    {
        if (!is_string($sSelect) || $sSelect == '') {
            throw new exceptionActiverecord('Database Invalid Query.');
        }

        $sType = strtoupper($sType);

        if (!in_array($sType, array('MAX', 'MIN', 'AVG', 'SUM'))) {
            throw new exceptionActiverecord('Invalid function type :' + $sType);
        }

        if ($sAlias=='') {
            $sAlias = $this->create_alias_from_table($sSelect);
        }

        if (in_array($sSelect, $this->aEncryptColumn)) {
            $sSelect = $this->getDecryptSql($sSelect);
        }

        $sSql = $sType.'('.$sSelect.') AS '.$sAlias;

        $this->aSelect[] = $sSql;

        return $this;
    }

    /**
     * Determines the alias name based on the table
     *
     * @param    String    $sItem
     *
     * @return    String
     */
    private function create_alias_from_table($sItem)
    {
        if (strpos($sItem, '.') !== false) {
            return end(explode('.', $sItem));
        }

        return $sItem;
    }

    /**
     * DISTINCT
     *
     * Sets a flag which tells the query string compiler to add DISTINCT
     *
     * @param    Boolean    $bVal
     *
     * @return    coreModelActiverecord
     */
    public function distinct($bVal = true)
    {
        $this->bDistinct = (is_bool($bVal))? $bVal : true;

        return $this;
    }

    /**
     * From
     *
     * Generates the FROM portion of the query
     *
     * @param    Mixed    $mFrom    can be a string or array
     *
     * @return    coreModelActiverecord
     */
    public function from($mFrom)
    {
        foreach ((array)$mFrom as $sVal) {
            if (strpos($sVal, ',') !== false) {
                foreach (explode(',', $sVal) as $v) {
                    $v = trim($v);
                    $this->track_aliases($v);

                    $this->aFrom[] = $v;
                }
            } else {
                $sVal = trim($sVal);

                $this->track_aliases($sVal);

                $this->aFrom[] = $sVal;
            }
        }

        return $this;
    }

    /**
     * Join
     *
     * Generates the JOIN portion of the query
     *
     * @param    String    $sTable    the table
     * @param    String    $sCond    the join condition
     * @param    String    $sType    the type of join
     *
     * @return    coreModelActiverecord
     */
    public function join($sTable, $sCond, $sType = '')
    {
        if ($sType != '') {
            $sType = strtoupper(trim($sType));

            if (!in_array($sType, array('LEFT', 'RIGHT', 'OUTER', 'INNER', 'LEFT OUTER', 'RIGHT OUTER'))) {
                $sType = '';
            } else {
                $sType .= ' ';
            }
        }

        $this->track_aliases($sTable);

        if (preg_match('/([\w\.]+)([\W\s]+)(.+)/', $sCond, $match)) {
            $sCond = $match[1].$match[2].$match[3];
        }

        $sJoin = $sType.'JOIN '.$sTable.' ON '.$sCond;

        $this->aJoin[] = $sJoin;

        return $this;
    }

    /**
     * Where
     *
     * Generates the WHERE portion of the query. Separates
     * multiple calls with AND
     *
     * @param    Mixed    $mKey
     * @param    Mixed    $mValue
     * @param    Mixed    $mEscape
     *
     * @return    coreModelActiverecord
     */
    public function where($mKey, $mValue = null, $mEscape = true)
    {
        return $this->whereSet($mKey, $mValue, 'AND ', $mEscape);
    }

    /**
     * OR Where
     *
     * Generates the WHERE portion of the query. Separates
     * multiple calls with OR
     *
     * @param    Mixed    $mKey
     * @param    Mixed    $mValue
     * @param    Mixed    $mEscape
     *
     * @return    coreModelActiverecord
     */
    public function or_where($mKey, $mValue = null, $mEscape = true)
    {
        return $this->whereSet($mKey, $mValue, 'OR ', $mEscape);
    }

    /**
     * Where
     *
     * Called by where() or orwhere()
     *
     * @param    Mixed    $mKey
     * @param    Mixed    $mValue
     * @param    String    $sType
     * @param    Mixed    $mEscape
     *
     * @return    coreModelActiverecord
     */
    private function whereSet($mKey, $mValue = null, $sType = 'AND ', $mEscape = null)
    {
        if (!is_array($mKey)) {
            $mKey = array($mKey => $mValue);
        }

        if (!is_bool($mEscape)) {
            $mEscape = $this->bProtectIdentifiers;
        }

        foreach ($mKey as $k => $v) {
            $prefix = empty($this->aWhere) ? '' : $sType;

            if ($v === null && !$this->has_operator($k)) {
                $k .= ' IS NULL';
            }

            if ($v !== null) {
                if ($this->has_operator($k)) {
                    $sColumnName = preg_replace('/(<|>|!|=|is null|is not null)/i', '', $k);
                    $sColumnName = trim($sColumnName);

                    if (in_array($sColumnName, $this->aEncryptColumn)) {
                        $sDecryptSql = $this->getDecryptSql($sColumnName);

                        $k = str_replace($sColumnName, $sDecryptSql, $k);
                    }
                }

                if (in_array($k, $this->aEncryptColumn)) {
                    $k = $this->getDecryptSql($k);
                }

                if ($mEscape === true) {
                    $v = ' '.$this->escape($v);
                }

                if (!$this->has_operator($k)) {
                    $k .= ' =';
                }
            }

            $this->aWhere[] = $prefix.$k.$v;
        }

        return $this;
    }

    /**
     * Tests whether the string has an SQL operator
     *
     * @param    String    $sStr
     *
     * @return    Boolean
     */
    function has_operator($sStr)
    {
        $sStr = trim($sStr);
        if (!preg_match('/(\s|<|>|!|=|is null|is not null)/i', $sStr)) {
            return FALSE;
        }

        return TRUE;
    }

    /**
     * Where_in
     *
     * Generates a WHERE field IN ('item', 'item') SQL query joined with
     * AND if appropriate
     *
     * @param    String    $sKey    The field to search
     * @param    Array    $aValues    The values searched on
     *
     * @return    coreModelActiverecord
     */
    public function where_in($sKey = null, $aValues = null)
    {
        return $this->where_in_common($sKey, $aValues);
    }

    /**
     * Where_in_or
     *
     * Generates a WHERE field IN ('item', 'item') SQL query joined with
     * OR if appropriate
     *
     * @param    String    $sKey    The field to search
     * @param    Array    $aValues    The values searched on
     *
     * @return    coreModelActiverecord
     */
    public function or_where_in($sKey = null, $aValues = null)
    {
        return $this->where_in_common($sKey, $aValues, false, 'OR ');
    }

    /**
     * Where_not_in
     *
     * Generates a WHERE field NOT IN ('item', 'item') SQL query joined
     * with AND if appropriate
     *
     * @param    String    $sKey    The field to search
     * @param    Array    $aValues    The values searched on
     *
     * @return    coreModelActiverecord
     */
    public function where_not_in($sKey = null, $aValues = null)
    {
        return $this->where_in_common($sKey, $aValues, true);
    }

    /**
     * Where_not_in_or
     *
     * Generates a WHERE field NOT IN ('item', 'item') SQL query joined
     * with OR if appropriate
     *
     * @param    String    $sKey    The field to search
     * @param    Array    $aValues    The values searched on
     *
     * @return    coreModelActiverecord
     */
    public function or_where_not_in($sKey = null, $aValues = null)
    {
        return $this->where_in_common($sKey, $aValues, true, 'OR ');
    }

    /**
     * Where_in
     *
     * Called by where_in, where_in_or, where_not_in, where_not_in_or
     *
     * @param    String    $sKey    The field to search
     * @param    Array    $aValues    The values searched on
     * @param    Boolean    $bNot    If the statement would be IN or NOT IN
     * @param    String    $sType    Operator
     *
     * @return    coreModelActiverecord
     */
    public function where_in_common($sKey = null, $aValues = null, $bNot = false, $sType = 'AND ')
    {
        if ($sKey === null OR $aValues === null) {
            return $this;
        }

        $sKey = (in_array($sKey, $this->aEncryptColumn))? $this->getDecryptSql($sKey) : $sKey;

        if (!is_array($aValues)) {
            $aValues = array($aValues);
        }

        $bNot = ($bNot) ? ' NOT' : '';

        foreach ($aValues as $value) {
            $this->aWherein[] = $this->escape($value);
        }

        $sPrefix = empty($this->aWhere) ? '' : $sType;

        $sWhere_in = $sPrefix.$sKey.$bNot.' IN ('.implode(', ', $this->aWherein).') ';

        $sWhere_in = trim($sWhere_in);
        $sLikeStatement = preg_replace('/\s\s+/', ' ', $sWhere_in);

        $this->aWhere[] = $sWhere_in;

        $this->aWherein = array();

        return $this;
    }

    /**
     * Like
     *
     * Generates a %LIKE% portion of the query. Separates
     * multiple calls with AND
     *
     * @param    Mixed    $mField
     * @param    Mixed    $mMatch
     * @param    String    $sSide
     *
     * @return    coreModelActiverecord
     */
    public function like($mField, $mMatch = '', $sSide = 'BOTH')
    {
        return $this->like_common($mField, $mMatch, 'AND ', $sSide);
    }

    /**
     * Not Like
     *
     * Generates a NOT LIKE portion of the query. Separates
     * multiple calls with AND
     *
     * @param    Mixed    $mField
     * @param    Mixed    $mMatch
     * @param    String    $sSide
     *
     * @return    coreModelActiverecord
     */
    public function not_like($mField, $mMatch = '', $sSide = 'BOTH')
    {
        return $this->like_common($mField, $mMatch, 'AND ', $sSide, 'NOT');
    }

    /**
     * OR Like
     *
     * Generates a %LIKE% portion of the query. Separates
     * multiple calls with OR
     *
     * @param    Mixed    $mField
     * @param    Mixed    $mMatch
     * @param    String    $sSide
     *
     * @return    coreModelActiverecord
     */
    public function or_like($mField, $mMatch = '', $sSide = 'BOTH')
    {
        return $this->like_common($mField, $mMatch, 'OR ', $sSide);
    }

    /**
     * OR Not Like
     *
     * Generates a NOT LIKE portion of the query. Separates
     * multiple calls with OR
     *
     * @param    Mixed    $mField
     * @param    Mixed    $mMatch
     * @param    String    $sSide
     *
     * @return    coreModelActiverecord
     */
    public function or_not_like($mField, $mMatch = '', $sSide = 'BOTH')
    {
        return $this->like_common($mField, $mMatch, 'OR ', $sSide, 'NOT');
    }

    /**
     * Like
     *
     * Called by like() or orlike()
     *
     * @param    Mixed    $mField
     * @param    Mixed    $mMatch
     * @param    String    $sType
     * @param    String    $sSide
     * @param    String    $sNot
     *
     * @return    coreModelActiverecord
     */
    private function like_common($mField, $mMatch = '', $sType = 'AND ', $sSide = 'BOTH', $sNot = '')
    {
        $sSide = strtoupper($sSide);

        if (!is_array($mField)) {
            $mField = array($mField => $mMatch);
        }

        foreach ($mField as $sKey=>$sValue) {
            $sKey = (in_array($sKey, $this->aEncryptColumn))? $this->getDecryptSql($sKey) : $sKey;

            $sPrefix = empty($this->aLike) ? '' : $sType;

            if ($sSide == 'BEFORE') {
                $sLikeStatement = $sPrefix." $sKey $sNot LIKE '%{$sValue}'";
            } else if ($sSide == 'AFTER') {
                $sLikeStatement = $sPrefix." $sKey $sNot LIKE '{$sValue}%'";
            } else {
                $sLikeStatement = $sPrefix." $sKey $sNot LIKE '%{$sValue}%'";
            }

            $sLikeStatement = trim($sLikeStatement);
            $sLikeStatement = preg_replace('/\s\s+/', ' ', $sLikeStatement);

            if ($this->sLikeEscapeStr != '')  $sLikeStatement = $sLikeStatement.sprintf($this->sLikeEscapeStr, $this->sLikeEscapeChr);

            $this->aLike[] = $sLikeStatement;
        }

        return $this;
    }

    /**
     * GROUP BY
     *
     * @param    String    $sBy
     *
     * @return    coreModelActiverecord
     */
    public function group_by($sBy)
    {
        if (is_string($sBy)) {
            $sBy = explode(',', $sBy);
        }

        foreach ($sBy as $sValue) {
            $sValue = trim($sValue);

            if ($sValue != '') {
                $this->aGroupby[] = $sValue;
            }
        }
        return $this;
    }

    /**
     * Sets the HAVING value
     *
     * Separates multiple calls with AND
     *
     * @param    String    $sKey
     * @param    String    $sValue
     * @param    Boolean    $bEscape
     *
     * @return    coreModelActiverecord
     */
    public function having($sKey, $sValue = '', $bEscape = true)
    {
        return $this->having_common($sKey, $sValue, 'AND ', $bEscape);
    }

    /**
     * Sets the OR HAVING value
     *
     * Separates multiple calls with OR
     *
     * @param    String    $sKey
     * @param    String    $sValue
     * @param    Boolean    $bEscape
     *
     * @return    coreModelActiverecord
     */
    public function or_having($sKey, $sValue = '', $bEscape = true)
    {
        return $this->having_common($sKey, $sValue, 'OR ', $bEscape);
    }

    /**
     * Sets the HAVING values
     *
     * Called by having() or or_having()
     *
     * @param    String    $sKey
     * @param    String    $sValue
     * @param    String    $sType
     * @param    Boolean    $bEscape
     *
     * @return    coreModelActiverecord
     */
    private function having_common($sKey, $sValue = '', $sType = 'AND ', $bEscape = true)
    {
        if (!is_array($sKey)) {
            $sKey = array($sKey => $sValue);
        }

        foreach ($sKey as $sKey => $sValue) {
            $sPrefix = empty($this->aHaving) ? '' : $sType;

            if (!$this->has_operator($sKey)) {
                $sKey .= ' = ';
            }

            if ($sValue != '') {
                /* Org.
                $sValue = ' '.$this->acDriver->escapeStr($sValue);
                */
                $sValue = ' '.$this->escape($sValue);
            }

            $this->aHaving[] = preg_replace('/\s\s+/', ' ', $sPrefix.$sKey.$sValue);
        }

        return $this;
    }

    /**
     * Sets the ORDER BY value
     *
     * @param    String    $sOrderby
     * @param    String    $sDirection    direction: asc or desc
     * @param    Boolean    $bEscape
     *
     * @return    coreModelActiverecord
     */
    public function order_by($sOrderby, $sDirection = '', $bEscape = true)
    {
        if (strtoupper($sDirection) == 'RANDOM') {
            $sOrderby = '';
            $sDirection = $this->acDriver->sRandomKeyword;
        } else if (trim($sDirection) != '') {
            $sDirection = (in_array(strtoupper(trim($sDirection)), array('ASC', 'DESC'), true)) ? ' '.$sDirection : ' ASC';
        }

        if (strpos($sOrderby, ',') !== false && $bEscape) {
            $aTemp = array();
            foreach (explode(',', $sOrderby) as $sPart) {
                $sPart = trim($sPart);
                if (!in_array($sPart, $this->aAliasedTables)) {
                    if ($bEscape===true) {
                        $sPart = trim($sPart);
                    }
                }

                $aTemp[] = $sPart;
            }

            $sOrderby = implode(', ', $aTemp);
        }

        $sOrderbyStatement = $sOrderby.$sDirection;

        $this->aOrderby[] = $sOrderbyStatement;

        return $this;
    }

    /**
     * Sets the LIMIT value
     *
     * @param    Integer    $iValue    the limit value
     * @param    Integer    $iOffset    the offset value
     *
     * @return    coreModelActiverecord
     */
    public function limit($iValue, $iOffset = '')
    {
        $this->iLimit = $iValue;

        if ($iOffset!='') {
            $this->iOffset = $iOffset;
        }

        return $this;
    }

    /**
     * Sets the OFFSET value
     *
     * @param    Integer    $iOffset    the offset value
     *
     * @return    coreModelActiverecord
     */
    public function offset($iOffset)
    {
        $this->iOffset = $iOffset;

        return $this;
    }

    /**
     * The "set" function.  Allows key/value pairs to be set for inserting or updating
     *
     * @param    Mixed    $mKey
     * @param    String    $sValue
     * @param    Boolean    $bEscape
     *
     * @return    coreModelActiverecord
     */
    public function set($mKey, $sValue = '', $bEscape = true)
    {
        $mKey = $this->object_to_array($mKey);

        if (!is_array($mKey)) {
            $mKey = array($mKey => $sValue);
        }

        foreach ($mKey as $sKey => $sValue) {
            if (in_array($sKey, $this->aEncryptColumn)) {
                $sValue = $this->getEncryptSql($this->acDriver->escapeStr($sValue));
            } else {
                $sValue = ($bEscape===false)? $sValue : $this->escape($sValue);
            }

            $sKey = $sKey;

            $this->aSet[$sKey] = $sValue;
        }

        return $this;
    }

    /**
     * Get
     *
     * Compiles the select statement based on the other functions called
     * and runs the query
     *
     * @param    String    $sTable    the table
     * @param    Integer    $iLimit    the limit clause
     * @param    Integer    $iOffset    the offset clause
     *
     * @return    Mixed
     */
    public function get($sTable = '', $iLimit = null, $iOffset = '')
    {
        if ($sTable!='') {
            $this->track_aliases($sTable);
            $this->from($sTable);
        }

        if ($iLimit !== null) {
            $this->limit($iLimit, $iOffset);
        }

        $sSql = $this->compile_select();

        $mResult = $this->query($sSql);

        $this->reset_select();

        return $mResult;
    }

    /**
     * "Count All Results" query
     *
     * Generates a platform-specific query string that counts all records
     * returned by an Active Record query.
     *
     * @param    String    $sTable
     *
     * @return    String
     */
    public function count_all_results($sTable = '')
    {
        if ($sTable != '') {
            $this->track_aliases($sTable);
            $this->from($sTable);
        }

        $sSql = $this->compile_select($this->acDriver->sCountString.'numrows');

        if ($this->bQueryMode!==false) {
            $this->reset_select();
            return $sSql;
        }

        $aResult = $this->query($sSql);

        $this->reset_select();

        return !empty($aResult[0]['numrows'])? $aResult[0]['numrows'] : false;
    }

    /**
     * Get_Where
     *
     * Allows the where clause, limit and offset to be added directly
     *
     * @param    String    $sTable    the where clause
     * @param    String    $sWhere
     * @param    Integer    $iLimit    the limit clause
     * @param    Integer    $iOffset    the offset clause
     *
     * @return    Mixed
     */
    public function get_where($sTable = '', $sWhere = null, $iLimit = null, $iOffset = '')
    {
        if ($sTable != '') {
            $this->from($sTable);
        }

        if ($sWhere !== null) {
            $this->where($sWhere);
        }

        if ($iLimit !== null) {
            $this->limit($iLimit, $iOffset);
        }

        $sSql = $this->compile_select();

        $result = $this->query($sSql);

        $this->reset_select();

        return $result;
    }

    /**
     * Insert
     *
     * Compiles an insert string and runs the query
     *
     * @param    String    $sTable    the table to retrieve the results from
     * @param    Array    $aSet    an associative array of insert values
     *
     * @return    Mixed
     */
    public function insert($sTable = '', $aSet = null)
    {
        if ($aSet !== null) {
            $this->set($aSet);
        }

        if (empty($this->aSet)) {
            throw new exceptionActiverecord('Database Must Use Columns.');
        }

        if ($sTable == '') {
            if (!isset($this->aFrom[0])) {
                throw new exceptionActiverecord('Database Must Use Table.');
            }

            $sTable = $this->aFrom[0];
        }

        $sSql = $this->acDriver->insert($sTable,
            array_keys($this->aSet),
            array_values($this->aSet));

        $this->reset_write();

        return $this->query($sSql);
    }

    /**
     * Update
     *
     * Compiles an update string and runs the query
     *
     * @param    String    $sTable    the table to retrieve the results from
     * @param    Array    $aSet    an associative array of update values
     * @param    Mixed    $mWhere    the where clause
     * @param    Integer    $iLimit
     *
     * @return    Mixed
     */
    public function update($sTable = '', $aSet = null, $mWhere = null, $iLimit = null)
    {
        if ($aSet !== null) {
            $this->set($aSet);
        }

        if (empty($this->aSet)) {
            throw new exceptionActiverecord('Database Muse Use Columns.');
        }

        if ($sTable == '') {
            if (!isset($this->aFrom[0])) {
                throw new exceptionActiverecord('Database Muse Use Table.');
            }

            $sTable = $this->aFrom[0];
        }

        if ($mWhere != null) {
            $this->where($mWhere);
        }

        if ($iLimit != null) {
            $this->limit($iLimit);
        }

        $sSql = $this->acDriver->update($sTable,
            $this->aSet,
            $this->aWhere,
            $this->aOrderby,
            $this->iLimit);

        $this->reset_write();

        return $this->query($sSql);
    }

    /**
     * Empty Table
     *
     * Compiles a delete string and runs "DELETE FROM table"
     *
     * @param    String    $sTable    the table to empty
     *
     * @return    Mixed
     */
    public function empty_table($sTable = '')
    {
        if ($sTable == '') {
            if (!isset($this->aFrom[0])) {
                throw new exceptionActiverecord('Database Must Set Table.');
            }

            $sTable = $this->aFrom[0];
        }

        $sql = $this->acDriver->delete($sTable);

        $this->reset_write();

        return $this->query($sql);
    }

    /**
     * Truncate
     *
     * Compiles a truncate string and runs the query
     * If the database does not support the truncate() command
     * This function maps to "DELETE FROM table"
     *
     * @param    String    $sTable    the table to truncate
     *
     * @return    Mixed
     */
    public function truncate($sTable = '')
    {
        if ($sTable == '') {
            if (!isset($this->aFrom[0])) {
                throw new exceptionActiverecord('Datase Must Set Table.');
            }

            $sTable = $this->aFrom[0];
        }

        $sSql = $this->acDriver->truncate($sTable);

        $this->reset_write();

        return $this->query($sSql);
    }

    /**
     * Delete
     *
     * Compiles a delete string and runs the query
     *
     * @param    Mixed    $mTable    the table(s) to delete from. String or array
     * @param    Mixed    $mWhere    the where clause
     * @param    Mixed    $mLimit    the limit clause
     * @param    Boolean    $bResetData
     *
     * @return    Mixed
     */
    public function delete($mTable = '', $mWhere = '', $mLimit = null, $bResetData = true)
    {
        if ($mTable == '') {
            if (!isset($this->aFrom[0])) {
                throw new exceptionActiverecord('Database Must Set Table.');
            }

            $mTable = $this->aFrom[0];
        } else if (is_array($mTable)) {
            foreach ($mTable as $sSingleTable) {
                $this->delete($sSingleTable, $mWhere, $mLimit, false);
            }

            $this->reset_write();
            return;
        }

        if ($mWhere != '') {
            $this->where($mWhere);
        }

        if ($mLimit != null) {
            $this->limit($mLimit);
        }

        if (empty($this->aWhere) && empty($this->aWherein)&& empty($this->aLike)) {
            throw new exceptionActiverecord('Database Delete Must Use Where.');
        }

        $sSql = $this->acDriver->delete($mTable, $this->aWhere, $this->aLike, $this->iLimit);

        if ($bResetData) {
            $this->reset_write();
        }

        return $this->query($sSql);
    }

    /**
     * DB Prefix
     *
     * Prepends a database prefix if one exists in configuration
     *
     * @param    String    $sTable    the table
     *
     * @return    String
     */
    public function dbprefix($sTable = '')
    {
        if ($sTable == '') {
            throw new exceptionActiverecord('Database Table Name Required.');
        }

        return $this->dbDriver->dbPrefix.$sTable;
    }

    /**
     * Track Aliases
     *
     * Used to track SQL statements written with aliased tables.
     *
     * @param    Mixed    $mTable    The table to inspect
     *
     * @return    String
     */
    private function track_aliases($mTable)
    {
        if (is_array($mTable)) {
            foreach ($mTable as $sTable) {
                $this->track_aliases($sTable);
            }
            return;
        }

        if (strpos($mTable, ',') !== false) {
            return $this->track_aliases(explode(',', $mTable));
        }

        if (strpos($mTable, ' ') !== false) {
            $mTable = preg_replace('/ AS /i', ' ', $mTable);

            $mTable = trim(strrchr($mTable, ' '));

            if (!in_array($mTable, $this->aAliasedTables)) {
                $this->aAliasedTables[] = $mTable;
            }
        }
    }

    /**
     * Compile the SELECT statement
     *
     * Generates a query string based on which functions were used.
     * Should not be called directly.  The get() function calls it.
     *
     * @param    Mixed    $mSelectOverride
     *
     * @return    String
     */
    private function compile_select($mSelectOverride = false)
    {
        if ($mSelectOverride !== false) {
            $sSql = $mSelectOverride;
        } else {
            $sSql = ( ! $this->bDistinct) ? 'SELECT ' : 'SELECT DISTINCT ';

            if (empty($this->aSelect)) {
                $sSql .= '*';
            } else {
                foreach ($this->aSelect as $key => $val) {
                    $this->aSelect[$key] = $val;
                }

                $sSql .= implode(', ', $this->aSelect);
            }
        }

        if (!empty($this->aFrom)) {
            $sSql .= "\nFROM ";

            $sSql .= $this->acDriver->fromTables($this->aFrom);
        }

        if (!empty($this->aJoin)) {
            $sSql .= "\n";

            $sSql .= implode("\n", $this->aJoin);
        }

        if (!empty($this->aWhere) || !empty($this->aLike)) {
            $sSql .= "\n";

            $sSql .= 'WHERE ';
        }

        $sSql .= implode("\n", $this->aWhere);

        if (!empty($this->aLike)) {
            if (!empty($this->aWhere)) {
                $sSql .= "\nAND ";
            }

            $sSql .= implode("\n", $this->aLike);
        }

        if (!empty($this->aGroupby)) {
            $sSql .= "\nGROUP BY ";

            $sSql .= implode(', ', $this->aGroupby);
        }

        if (!empty($this->aHaving)) {
            $sSql .= "\nHAVING ";
            $sSql .= implode("\n", $this->aHaving);
        }

        if (!empty($this->aOrderby)) {
            $sSql .= "\nORDER BY ";
            $sSql .= implode(', ', $this->aOrderby);

            if ($this->sOrder !== FALSE)    $sSql .= ($this->sOrder == 'desc') ? ' DESC' : ' ASC';
        }

        /**
         * PgSQL Limit - SELECT [] FROM [] LIMIT {number|ALL} [OFFSET number]
         * MySQL Limit - SELECT [] FROM [] LIMIT {[offset,] row_count | row_count OFFSET offset}
         */
        if (!empty($this->iLimit)) {
            $sSql .= "\n";
            $sSql = $this->acDriver->limit($sSql, $this->iLimit, $this->iOffset);
        }

        return $sSql;
    }

    /**
     * Object to Array
     *
     * Takes an object as input and converts the class variables to array key/vals
     *
     * @param    Object    $object
     *
     * @return    Array
     */
    public function object_to_array($object)
    {
        if (!is_object($object)) {
            return $object;
        }

        $array = array();
        foreach (get_object_vars($object) as $key => $val) {
            if (!is_object($val) && ! is_array($val) && $key != '_parent_name' && $key != '_ci_scaffolding' && $key != '_ci_scaff_table') {
                $array[$key] = $val;
            }
        }

        return $array;
    }

    /**
     * Resets the active record values.  Called by the get() function
     *
     * @param    Array    An array of fields to reset
     *
     * @return    void
     */
    private function reset_run($aResetItems)
    {
        if (!empty($this->aStoreArray)) {
            foreach ($aResetItems as $sItem => $mDefaultValue) {
                if (!in_array($sItem, $this->aStoreArray)) {
                    $this->$sItem = $mDefaultValue;
                }
            }
        } else {
            foreach ($aResetItems as $sItem => $mDefaultValue) {
                $this->$sItem = $mDefaultValue;
            }
        }
    }

    /**
     * Resets the active record values.  Called by the get() function
     *
     * @return    void
     */
    private function reset_select()
    {
        $aResetItems = array(
            'aEncryptColumn'   => array(),
            'aSelect'    => array(),
            'aFrom'        => array(),
            'aJoin'        => array(),
            'aWhere'    => array(),
            'aLike'        => array(),
            'aGroupby'        => array(),
            'aHaving'    => array(),
            'aOrderby'        => array(),
            'aWherein'        => array(),
            'aAliasedTables'    => array(),
            'bDistinct'        => false,
            'iLimit'    => false,
            'iOffset'    => false,
            'sOrder'    => false,
        );

        $this->reset_run($aResetItems);
    }

    /**
     * Resets the active record "write" values.
     *
     * Called by the insert() update() and delete() functions
     *
     * @return    void
     */
    private function reset_write()
    {
        $aResetItems = array(
            'aEncryptColumn'   => array(),
            'aSet'            => array(),
            'aFrom'            => array(),
            'aWhere'        => array(),
            'aLike'            => array(),
            'aOrderby'            => array(),
            'iLimit'        => false,
            'sOrder'        => false
        );

        $this->reset_run($aResetItems);
    }

    /**
     * "Smart" Escape String
     *
     * Escapes data based on type
     * Sets boolean and null types
     *
     * @param    String    $sQuery    An SQL query string
     *
     * @return    Mixed
     */
    public function escape($sQuery)
    {
        if (is_string($sQuery)) {
            $sQuery = "'".$this->acDriver->escapeStr($sQuery)."'";
        } else if (is_bool($sQuery)) {
            $sQuery = ($sQuery === FALSE) ? 0 : 1;
        } else if ($sQuery === null) {
            $sQuery = 'NULL';
        }

        return $sQuery;
    }

    /**
     * Determines if a query is a "write" type.
     *
     * @param    String    $sQuery    An SQL query string
     *
     * @return    Boolean
     */
    public function is_write_type($sQuery)
    {
        if (!preg_match('/^\s*"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD DATA|COPY|ALTER|GRANT|REVOKE|LOCK|UNLOCK)\s+/i', $sQuery)) {
            return false;
        }

        return true;
    }

    /**
     * "Count All" query
     *
     * Generates a platform-specific query string that counts all records in
     * the specified database
     *
     * @param    String    $sTable
     *
     * @return    Integer
     */
    public function count_all($sTable = '')
    {
        if ($sTable == '') {
            return 0;
        }

        $sSql = $this->acDriver->sCountString.'numrows FROM '.$sTable;

        if ($this->bQueryMode!==false) {
            return $sSql;
        }

        $aResult = $this->query($sSql);

        return !empty($aResult[0]['numrows'])? $aResult[0]['numrows'] : false;
    }

    /**
     * Getting... Encrypt Query
     *
     * @param    Mixed    $mData
     *
     * @return    String
     */
    public function getEncryptSql($mData)
    {
        return $this->dbDriver->encrypt($mData, $this->sEncryptKey, $this->sEncryptIV);
    }

    /**
     * Getting... Decrypt Query
     *
     * @param    String    $sColumnName
     *
     * @return     String
     */
    public function getDecryptSql($sColumnName, $sAs='')
    {
        $sEncryptSql = $this->dbDriver->decrypt($sColumnName, $this->sEncryptKey, $this->sEncryptIV);

        return (empty($sAs))? $sEncryptSql : $sEncryptSql.' AS '.trim($sAs);
    }

    /**
     * Getting... Escape Character
     *
     * @return    String
     */
    public function getEscapeChar()
    {
        return $this->acDriver->sEscapeChar;
    }
}
